use crate::{
    Vim,
    motion::{self, Motion},
    object::{Object, surrounding_markers},
    state::Mode,
};
use editor::{Bias, MultiBufferOffset, movement};
use gpui::{Context, Window};
use language::BracketPair;

use std::sync::Arc;

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SurroundsType {
    Motion(Motion),
    Object(Object, bool),
    Selection,
}

impl Vim {
    pub fn add_surrounds(
        &mut self,
        text: Arc<str>,
        target: SurroundsType,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        self.stop_recording(cx);
        let count = Vim::take_count(cx);
        let forced_motion = Vim::take_forced_motion(cx);
        let mode = self.mode;
        self.update_editor(cx, |_, editor, cx| {
            let text_layout_details = editor.text_layout_details(window);
            editor.transact(window, cx, |editor, window, cx| {
                editor.set_clip_at_line_ends(false, cx);

                let pair = match find_surround_pair(&all_support_surround_pair(), &text) {
                    Some(pair) => pair.clone(),
                    None => BracketPair {
                        start: text.to_string(),
                        end: text.to_string(),
                        close: true,
                        surround: true,
                        newline: false,
                    },
                };
                let surround = pair.end != surround_alias((*text).as_ref());
                let display_map = editor.display_snapshot(cx);
                let display_selections = editor.selections.all_adjusted_display(&display_map);
                let mut edits = Vec::new();
                let mut anchors = Vec::new();

                for selection in &display_selections {
                    let range = match &target {
                        SurroundsType::Object(object, around) => {
                            object.range(&display_map, selection.clone(), *around, None)
                        }
                        SurroundsType::Motion(motion) => {
                            motion
                                .range(
                                    &display_map,
                                    selection.clone(),
                                    count,
                                    &text_layout_details,
                                    forced_motion,
                                )
                                .map(|(mut range, _)| {
                                    // The Motion::CurrentLine operation will contain the newline of the current line and leading/trailing whitespace
                                    if let Motion::CurrentLine = motion {
                                        range.start = motion::first_non_whitespace(
                                            &display_map,
                                            false,
                                            range.start,
                                        );
                                        range.end = movement::saturating_right(
                                            &display_map,
                                            motion::last_non_whitespace(&display_map, range.end, 1),
                                        );
                                    }
                                    range
                                })
                        }
                        SurroundsType::Selection => Some(selection.range()),
                    };

                    if let Some(range) = range {
                        let start = range.start.to_offset(&display_map, Bias::Right);
                        let end = range.end.to_offset(&display_map, Bias::Left);
                        let (start_cursor_str, end_cursor_str) = if mode == Mode::VisualLine {
                            (format!("{}\n", pair.start), format!("\n{}", pair.end))
                        } else {
                            let maybe_space = if surround { " " } else { "" };
                            (
                                format!("{}{}", pair.start, maybe_space),
                                format!("{}{}", maybe_space, pair.end),
                            )
                        };
                        let start_anchor = display_map.buffer_snapshot().anchor_before(start);

                        edits.push((start..start, start_cursor_str));
                        edits.push((end..end, end_cursor_str));
                        anchors.push(start_anchor..start_anchor);
                    } else {
                        let start_anchor = display_map
                            .buffer_snapshot()
                            .anchor_before(selection.head().to_offset(&display_map, Bias::Left));
                        anchors.push(start_anchor..start_anchor);
                    }
                }

                editor.edit(edits, cx);
                editor.set_clip_at_line_ends(true, cx);
                editor.change_selections(Default::default(), window, cx, |s| {
                    if mode == Mode::VisualBlock {
                        s.select_anchor_ranges(anchors.into_iter().take(1))
                    } else {
                        s.select_anchor_ranges(anchors)
                    }
                });
            });
        });
        self.switch_mode(Mode::Normal, false, window, cx);
    }

    pub fn delete_surrounds(
        &mut self,
        text: Arc<str>,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        self.stop_recording(cx);

        // only legitimate surrounds can be removed
        let pair = match find_surround_pair(&all_support_surround_pair(), &text) {
            Some(pair) => pair.clone(),
            None => return,
        };
        let pair_object = match pair_to_object(&pair) {
            Some(pair_object) => pair_object,
            None => return,
        };
        let surround = pair.end != *text;

        self.update_editor(cx, |_, editor, cx| {
            editor.transact(window, cx, |editor, window, cx| {
                editor.set_clip_at_line_ends(false, cx);

                let display_map = editor.display_snapshot(cx);
                let display_selections = editor.selections.all_display(&display_map);
                let mut edits = Vec::new();
                let mut anchors = Vec::new();

                for selection in &display_selections {
                    let start = selection.start.to_offset(&display_map, Bias::Left);
                    if let Some(range) =
                        pair_object.range(&display_map, selection.clone(), true, None)
                    {
                        // If the current parenthesis object is single-line,
                        // then we need to filter whether it is the current line or not
                        if !pair_object.is_multiline() {
                            let is_same_row = selection.start.row() == range.start.row()
                                && selection.end.row() == range.end.row();
                            if !is_same_row {
                                anchors.push(start..start);
                                continue;
                            }
                        }
                        // This is a bit cumbersome, and it is written to deal with some special cases, as shown below
                        // hello«ˇ  "hello in a word"  »again.
                        // Sometimes the expand_selection will not be matched at both ends, and there will be extra spaces
                        // In order to be able to accurately match and replace in this case, some cumbersome methods are used
                        let mut chars_and_offset = display_map
                            .buffer_chars_at(range.start.to_offset(&display_map, Bias::Left))
                            .peekable();
                        while let Some((ch, offset)) = chars_and_offset.next() {
                            if ch.to_string() == pair.start {
                                let start = offset;
                                let mut end = start + 1usize;
                                if surround
                                    && let Some((next_ch, _)) = chars_and_offset.peek()
                                    && next_ch.eq(&' ')
                                {
                                    end += 1;
                                }
                                edits.push((start..end, ""));
                                anchors.push(start..start);
                                break;
                            }
                        }
                        let mut reverse_chars_and_offsets = display_map
                            .reverse_buffer_chars_at(range.end.to_offset(&display_map, Bias::Left))
                            .peekable();
                        while let Some((ch, offset)) = reverse_chars_and_offsets.next() {
                            if ch.to_string() == pair.end {
                                let mut start = offset;
                                let end = start + 1usize;
                                if surround
                                    && let Some((next_ch, _)) = reverse_chars_and_offsets.peek()
                                    && next_ch.eq(&' ')
                                {
                                    start -= 1;
                                }
                                edits.push((start..end, ""));
                                break;
                            }
                        }
                    } else {
                        anchors.push(start..start);
                    }
                }

                editor.change_selections(Default::default(), window, cx, |s| {
                    s.select_ranges(anchors);
                });
                edits.sort_by_key(|(range, _)| range.start);
                editor.edit(edits, cx);
                editor.set_clip_at_line_ends(true, cx);
            });
        });
    }

    pub fn change_surrounds(
        &mut self,
        text: Arc<str>,
        target: Object,
        opening: bool,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        if let Some(will_replace_pair) = self.object_to_bracket_pair(target, cx) {
            self.stop_recording(cx);
            self.update_editor(cx, |_, editor, cx| {
                editor.transact(window, cx, |editor, window, cx| {
                    editor.set_clip_at_line_ends(false, cx);

                    let pair = match find_surround_pair(&all_support_surround_pair(), &text) {
                        Some(pair) => pair.clone(),
                        None => BracketPair {
                            start: text.to_string(),
                            end: text.to_string(),
                            close: true,
                            surround: true,
                            newline: false,
                        },
                    };

                    // A single space should be added if the new surround is a
                    // bracket and not a quote (pair.start != pair.end) and if
                    // the bracket used is the opening bracket.
                    let add_space =
                        !(pair.start == pair.end) && (pair.end != surround_alias((*text).as_ref()));

                    // Space should be preserved if either the surrounding
                    // characters being updated are quotes
                    // (will_replace_pair.start == will_replace_pair.end) or if
                    // the bracket used in the command is not an opening
                    // bracket.
                    let preserve_space =
                        will_replace_pair.start == will_replace_pair.end || !opening;

                    let display_map = editor.display_snapshot(cx);
                    let selections = editor.selections.all_adjusted_display(&display_map);
                    let mut edits = Vec::new();
                    let mut anchors = Vec::new();

                    for selection in &selections {
                        let start = selection.start.to_offset(&display_map, Bias::Left);
                        if let Some(range) =
                            target.range(&display_map, selection.clone(), true, None)
                        {
                            if !target.is_multiline() {
                                let is_same_row = selection.start.row() == range.start.row()
                                    && selection.end.row() == range.end.row();
                                if !is_same_row {
                                    anchors.push(start..start);
                                    continue;
                                }
                            }

                            // Keeps track of the length of the string that is
                            // going to be edited on the start so we can ensure
                            // that the end replacement string does not exceed
                            // this value. Helpful when dealing with newlines.
                            let mut edit_len = 0;
                            let mut open_range_end = MultiBufferOffset(0);
                            let mut chars_and_offset = display_map
                                .buffer_chars_at(range.start.to_offset(&display_map, Bias::Left))
                                .peekable();

                            while let Some((ch, offset)) = chars_and_offset.next() {
                                if ch.to_string() == will_replace_pair.start {
                                    let mut open_str = pair.start.clone();
                                    let start = offset;
                                    open_range_end = start + 1usize;
                                    while let Some((next_ch, _)) = chars_and_offset.next()
                                        && next_ch == ' '
                                    {
                                        open_range_end += 1;

                                        if preserve_space {
                                            open_str.push(next_ch);
                                        }
                                    }

                                    if add_space {
                                        open_str.push(' ');
                                    };

                                    edit_len = open_range_end - start;
                                    edits.push((start..open_range_end, open_str));
                                    anchors.push(start..start);
                                    break;
                                }
                            }

                            let mut reverse_chars_and_offsets = display_map
                                .reverse_buffer_chars_at(
                                    range.end.to_offset(&display_map, Bias::Left),
                                )
                                .peekable();
                            while let Some((ch, offset)) = reverse_chars_and_offsets.next() {
                                if ch.to_string() == will_replace_pair.end {
                                    let mut close_str = String::new();
                                    let mut start = offset;
                                    let end = start + 1usize;
                                    while let Some((next_ch, _)) = reverse_chars_and_offsets.next()
                                        && next_ch == ' '
                                        && close_str.len() < edit_len - 1
                                        && start > open_range_end
                                    {
                                        start -= 1;

                                        if preserve_space {
                                            close_str.push(next_ch);
                                        }
                                    }

                                    if add_space {
                                        close_str.push(' ');
                                    };

                                    close_str.push_str(&pair.end);
                                    edits.push((start..end, close_str));
                                    break;
                                }
                            }
                        } else {
                            anchors.push(start..start);
                        }
                    }

                    let stable_anchors = editor
                        .selections
                        .disjoint_anchors_arc()
                        .iter()
                        .map(|selection| {
                            let start = selection.start.bias_left(&display_map.buffer_snapshot());
                            start..start
                        })
                        .collect::<Vec<_>>();
                    edits.sort_by_key(|(range, _)| range.start);
                    editor.edit(edits, cx);
                    editor.set_clip_at_line_ends(true, cx);
                    editor.change_selections(Default::default(), window, cx, |s| {
                        s.select_anchor_ranges(stable_anchors);
                    });
                });
            });
        }
    }

    /// Checks if any of the current cursors are surrounded by a valid pair of brackets.
    ///
    /// This method supports multiple cursors and checks each cursor for a valid pair of brackets.
    /// A pair of brackets is considered valid if it is well-formed and properly closed.
    ///
    /// If a valid pair of brackets is found, the method returns `true` and the cursor is automatically moved to the start of the bracket pair.
    /// If no valid pair of brackets is found for any cursor, the method returns `false`.
    pub fn check_and_move_to_valid_bracket_pair(
        &mut self,
        object: Object,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) -> bool {
        let mut valid = false;
        if let Some(pair) = self.object_to_bracket_pair(object, cx) {
            self.update_editor(cx, |_, editor, cx| {
                editor.transact(window, cx, |editor, window, cx| {
                    editor.set_clip_at_line_ends(false, cx);
                    let display_map = editor.display_snapshot(cx);
                    let selections = editor.selections.all_adjusted_display(&display_map);
                    let mut anchors = Vec::new();

                    for selection in &selections {
                        let start = selection.start.to_offset(&display_map, Bias::Left);
                        if let Some(range) =
                            object.range(&display_map, selection.clone(), true, None)
                        {
                            // If the current parenthesis object is single-line,
                            // then we need to filter whether it is the current line or not
                            if object.is_multiline()
                                || (!object.is_multiline()
                                    && selection.start.row() == range.start.row()
                                    && selection.end.row() == range.end.row())
                            {
                                valid = true;
                                let chars_and_offset = display_map
                                    .buffer_chars_at(
                                        range.start.to_offset(&display_map, Bias::Left),
                                    )
                                    .peekable();
                                for (ch, offset) in chars_and_offset {
                                    if ch.to_string() == pair.start {
                                        anchors.push(offset..offset);
                                        break;
                                    }
                                }
                            } else {
                                anchors.push(start..start)
                            }
                        } else {
                            anchors.push(start..start)
                        }
                    }
                    editor.change_selections(Default::default(), window, cx, |s| {
                        s.select_ranges(anchors);
                    });
                    editor.set_clip_at_line_ends(true, cx);
                });
            });
        }
        valid
    }

    fn object_to_bracket_pair(
        &self,
        object: Object,
        cx: &mut Context<Self>,
    ) -> Option<BracketPair> {
        match object {
            Object::Quotes => Some(BracketPair {
                start: "'".to_string(),
                end: "'".to_string(),
                close: true,
                surround: true,
                newline: false,
            }),
            Object::BackQuotes => Some(BracketPair {
                start: "`".to_string(),
                end: "`".to_string(),
                close: true,
                surround: true,
                newline: false,
            }),
            Object::DoubleQuotes => Some(BracketPair {
                start: "\"".to_string(),
                end: "\"".to_string(),
                close: true,
                surround: true,
                newline: false,
            }),
            Object::VerticalBars => Some(BracketPair {
                start: "|".to_string(),
                end: "|".to_string(),
                close: true,
                surround: true,
                newline: false,
            }),
            Object::Parentheses => Some(BracketPair {
                start: "(".to_string(),
                end: ")".to_string(),
                close: true,
                surround: true,
                newline: false,
            }),
            Object::SquareBrackets => Some(BracketPair {
                start: "[".to_string(),
                end: "]".to_string(),
                close: true,
                surround: true,
                newline: false,
            }),
            Object::CurlyBrackets { .. } => Some(BracketPair {
                start: "{".to_string(),
                end: "}".to_string(),
                close: true,
                surround: true,
                newline: false,
            }),
            Object::AngleBrackets => Some(BracketPair {
                start: "<".to_string(),
                end: ">".to_string(),
                close: true,
                surround: true,
                newline: false,
            }),
            Object::AnyBrackets => {
                // If we're dealing with `AnyBrackets`, which can map to multiple
                // bracket pairs, we'll need to first determine which `BracketPair` to
                // target.
                // As such, we keep track of the smallest range size, so
                // that in cases like `({ name: "John" })` if the cursor is
                // inside the curly brackets, we target the curly brackets
                // instead of the parentheses.
                let mut bracket_pair = None;
                let mut min_range_size = usize::MAX;

                let _ = self.editor.update(cx, |editor, cx| {
                    let display_map = editor.display_snapshot(cx);
                    let selections = editor.selections.all_adjusted_display(&display_map);
                    // Even if there's multiple cursors, we'll simply rely on
                    // the first one to understand what bracket pair to map to.
                    // I believe we could, if worth it, go one step above and
                    // have a `BracketPair` per selection, so that `AnyBracket`
                    // could work in situations where the transformation below
                    // could be done.
                    //
                    // ```
                    // (< name:ˇ'Zed' >)
                    // <[ name:ˇ'DeltaDB' ]>
                    // ```
                    //
                    // After using `csb{`:
                    //
                    // ```
                    // (ˇ{ name:'Zed' })
                    // <ˇ{ name:'DeltaDB' }>
                    // ```
                    if let Some(selection) = selections.first() {
                        let relative_to = selection.head();
                        let bracket_pairs = [('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')];
                        let cursor_offset = relative_to.to_offset(&display_map, Bias::Left);

                        for &(open, close) in bracket_pairs.iter() {
                            if let Some(range) = surrounding_markers(
                                &display_map,
                                relative_to,
                                true,
                                false,
                                open,
                                close,
                            ) {
                                let start_offset = range.start.to_offset(&display_map, Bias::Left);
                                let end_offset = range.end.to_offset(&display_map, Bias::Right);

                                if cursor_offset >= start_offset && cursor_offset <= end_offset {
                                    let size = end_offset - start_offset;
                                    if size < min_range_size {
                                        min_range_size = size;
                                        bracket_pair = Some(BracketPair {
                                            start: open.to_string(),
                                            end: close.to_string(),
                                            close: true,
                                            surround: true,
                                            newline: false,
                                        })
                                    }
                                }
                            }
                        }
                    }
                });

                bracket_pair
            }
            _ => None,
        }
    }
}

fn find_surround_pair<'a>(pairs: &'a [BracketPair], ch: &str) -> Option<&'a BracketPair> {
    pairs
        .iter()
        .find(|pair| pair.start == surround_alias(ch) || pair.end == surround_alias(ch))
}

fn surround_alias(ch: &str) -> &str {
    match ch {
        "b" => ")",
        "B" => "}",
        "a" => ">",
        "r" => "]",
        _ => ch,
    }
}

fn all_support_surround_pair() -> Vec<BracketPair> {
    vec![
        BracketPair {
            start: "{".into(),
            end: "}".into(),
            close: true,
            surround: true,
            newline: false,
        },
        BracketPair {
            start: "'".into(),
            end: "'".into(),
            close: true,
            surround: true,
            newline: false,
        },
        BracketPair {
            start: "`".into(),
            end: "`".into(),
            close: true,
            surround: true,
            newline: false,
        },
        BracketPair {
            start: "\"".into(),
            end: "\"".into(),
            close: true,
            surround: true,
            newline: false,
        },
        BracketPair {
            start: "(".into(),
            end: ")".into(),
            close: true,
            surround: true,
            newline: false,
        },
        BracketPair {
            start: "|".into(),
            end: "|".into(),
            close: true,
            surround: true,
            newline: false,
        },
        BracketPair {
            start: "[".into(),
            end: "]".into(),
            close: true,
            surround: true,
            newline: false,
        },
        BracketPair {
            start: "<".into(),
            end: ">".into(),
            close: true,
            surround: true,
            newline: false,
        },
    ]
}

fn pair_to_object(pair: &BracketPair) -> Option<Object> {
    match pair.start.as_str() {
        "'" => Some(Object::Quotes),
        "`" => Some(Object::BackQuotes),
        "\"" => Some(Object::DoubleQuotes),
        "|" => Some(Object::VerticalBars),
        "(" => Some(Object::Parentheses),
        "[" => Some(Object::SquareBrackets),
        "{" => Some(Object::CurlyBrackets),
        "<" => Some(Object::AngleBrackets),
        _ => None,
    }
}

#[cfg(test)]
mod test {
    use gpui::KeyBinding;
    use indoc::indoc;

    use crate::{PushAddSurrounds, object::AnyBrackets, state::Mode, test::VimTestContext};

    #[gpui::test]
    async fn test_add_surrounds(cx: &mut gpui::TestAppContext) {
        let mut cx = VimTestContext::new(cx, true).await;

        // test add surrounds with around
        cx.set_state(
            indoc! {"
            The quˇick brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("y s i w {");
        cx.assert_state(
            indoc! {"
            The ˇ{ quick } brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );

        // test add surrounds not with around
        cx.set_state(
            indoc! {"
            The quˇick brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("y s i w }");
        cx.assert_state(
            indoc! {"
            The ˇ{quick} brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );

        // test add surrounds with motion
        cx.set_state(
            indoc! {"
            The quˇick brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("y s $ }");
        cx.assert_state(
            indoc! {"
            The quˇ{ick brown}
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );

        // test add surrounds with multi cursor
        cx.set_state(
            indoc! {"
            The quˇick brown
            fox jumps over
            the laˇzy dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("y s i w '");
        cx.assert_state(
            indoc! {"
            The ˇ'quick' brown
            fox jumps over
            the ˇ'lazy' dog."},
            Mode::Normal,
        );

        // test multi cursor add surrounds with motion
        cx.set_state(
            indoc! {"
            The quˇick brown
            fox jumps over
            the laˇzy dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("y s $ '");
        cx.assert_state(
            indoc! {"
            The quˇ'ick brown'
            fox jumps over
            the laˇ'zy dog.'"},
            Mode::Normal,
        );

        // test multi cursor add surrounds with motion and custom string
        cx.set_state(
            indoc! {"
            The quˇick brown
            fox jumps over
            the laˇzy dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("y s $ 1");
        cx.assert_state(
            indoc! {"
            The quˇ1ick brown1
            fox jumps over
            the laˇ1zy dog.1"},
            Mode::Normal,
        );

        // test add surrounds with motion current line
        cx.set_state(
            indoc! {"
            The quˇick brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("y s s {");
        cx.assert_state(
            indoc! {"
            ˇ{ The quick brown }
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );

        cx.set_state(
            indoc! {"
                The quˇick brown•
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("y s s {");
        cx.assert_state(
            indoc! {"
                ˇ{ The quick brown }•
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("2 y s s )");
        cx.assert_state(
            indoc! {"
                ˇ({ The quick brown }•
            fox jumps over)
            the lazy dog."},
            Mode::Normal,
        );

        // test add surrounds around object
        cx.set_state(
            indoc! {"
            The [quˇick] brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("y s a ] )");
        cx.assert_state(
            indoc! {"
            The ˇ([quick]) brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );

        // test add surrounds inside object
        cx.set_state(
            indoc! {"
            The [quˇick] brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("y s i ] )");
        cx.assert_state(
            indoc! {"
            The [ˇ(quick)] brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );
    }

    #[gpui::test]
    async fn test_add_surrounds_visual(cx: &mut gpui::TestAppContext) {
        let mut cx = VimTestContext::new(cx, true).await;

        cx.update(|_, cx| {
            cx.bind_keys([KeyBinding::new(
                "shift-s",
                PushAddSurrounds {},
                Some("vim_mode == visual"),
            )])
        });

        // test add surrounds with around
        cx.set_state(
            indoc! {"
            The quˇick brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("v i w shift-s {");
        cx.assert_state(
            indoc! {"
            The ˇ{ quick } brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );

        // test add surrounds not with around
        cx.set_state(
            indoc! {"
            The quˇick brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("v i w shift-s }");
        cx.assert_state(
            indoc! {"
            The ˇ{quick} brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );

        // test add surrounds with motion
        cx.set_state(
            indoc! {"
            The quˇick brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("v e shift-s }");
        cx.assert_state(
            indoc! {"
            The quˇ{ick} brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );

        // test add surrounds with multi cursor
        cx.set_state(
            indoc! {"
            The quˇick brown
            fox jumps over
            the laˇzy dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("v i w shift-s '");
        cx.assert_state(
            indoc! {"
            The ˇ'quick' brown
            fox jumps over
            the ˇ'lazy' dog."},
            Mode::Normal,
        );

        // test add surrounds with visual block
        cx.set_state(
            indoc! {"
            The quˇick brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("ctrl-v i w j j shift-s '");
        cx.assert_state(
            indoc! {"
            The ˇ'quick' brown
            fox 'jumps' over
            the 'lazy 'dog."},
            Mode::Normal,
        );

        // test add surrounds with visual line
        cx.set_state(
            indoc! {"
            The quˇick brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("j shift-v shift-s '");
        cx.assert_state(
            indoc! {"
            The quick brown
            ˇ'
            fox jumps over
            '
            the lazy dog."},
            Mode::Normal,
        );
    }

    #[gpui::test]
    async fn test_delete_surrounds(cx: &mut gpui::TestAppContext) {
        let mut cx = VimTestContext::new(cx, true).await;

        // test delete surround
        cx.set_state(
            indoc! {"
            The {quˇick} brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("d s {");
        cx.assert_state(
            indoc! {"
            The ˇquick brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );

        // test delete not exist surrounds
        cx.set_state(
            indoc! {"
            The {quˇick} brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("d s [");
        cx.assert_state(
            indoc! {"
            The {quˇick} brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );

        // test delete surround forward exist, in the surrounds plugin of other editors,
        // the bracket pair in front of the current line will be deleted here, which is not implemented at the moment
        cx.set_state(
            indoc! {"
            The {quick} brˇown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("d s {");
        cx.assert_state(
            indoc! {"
            The {quick} brˇown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );

        // test cursor delete inner surrounds
        cx.set_state(
            indoc! {"
            The { quick brown
            fox jumˇps over }
            the lazy dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("d s {");
        cx.assert_state(
            indoc! {"
            The ˇquick brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );

        // test multi cursor delete surrounds
        cx.set_state(
            indoc! {"
            The [quˇick] brown
            fox jumps over
            the [laˇzy] dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("d s ]");
        cx.assert_state(
            indoc! {"
            The ˇquick brown
            fox jumps over
            the ˇlazy dog."},
            Mode::Normal,
        );

        // test multi cursor delete surrounds with around
        cx.set_state(
            indoc! {"
            Tˇhe [ quick ] brown
            fox jumps over
            the [laˇzy] dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("d s [");
        cx.assert_state(
            indoc! {"
            The ˇquick brown
            fox jumps over
            the ˇlazy dog."},
            Mode::Normal,
        );

        cx.set_state(
            indoc! {"
            Tˇhe [ quick ] brown
            fox jumps over
            the [laˇzy ] dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("d s [");
        cx.assert_state(
            indoc! {"
            The ˇquick brown
            fox jumps over
            the ˇlazy dog."},
            Mode::Normal,
        );

        // test multi cursor delete different surrounds
        // the pair corresponding to the two cursors is the same,
        // so they are combined into one cursor
        cx.set_state(
            indoc! {"
            The [quˇick] brown
            fox jumps over
            the {laˇzy} dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("d s {");
        cx.assert_state(
            indoc! {"
            The [quick] brown
            fox jumps over
            the ˇlazy dog."},
            Mode::Normal,
        );

        // test delete surround with multi cursor and nest surrounds
        cx.set_state(
            indoc! {"
            fn test_surround() {
                ifˇ 2 > 1 {
                    ˇprintln!(\"it is fine\");
                };
            }"},
            Mode::Normal,
        );
        cx.simulate_keystrokes("d s }");
        cx.assert_state(
            indoc! {"
            fn test_surround() ˇ
                if 2 > 1 ˇ
                    println!(\"it is fine\");
                ;
            "},
            Mode::Normal,
        );
    }

    #[gpui::test]
    async fn test_change_surrounds(cx: &mut gpui::TestAppContext) {
        let mut cx = VimTestContext::new(cx, true).await;

        cx.set_state(
            indoc! {"
            The {quˇick} brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("c s { [");
        cx.assert_state(
            indoc! {"
            The ˇ[ quick ] brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );

        // test multi cursor change surrounds
        cx.set_state(
            indoc! {"
            The {quˇick} brown
            fox jumps over
            the {laˇzy} dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("c s { [");
        cx.assert_state(
            indoc! {"
            The ˇ[ quick ] brown
            fox jumps over
            the ˇ[ lazy ] dog."},
            Mode::Normal,
        );

        // test multi cursor delete different surrounds with after cursor
        cx.set_state(
            indoc! {"
            Thˇe {quick} brown
            fox jumps over
            the {laˇzy} dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("c s { [");
        cx.assert_state(
            indoc! {"
            The ˇ[ quick ] brown
            fox jumps over
            the ˇ[ lazy ] dog."},
            Mode::Normal,
        );

        // test multi cursor change surrount with not around
        cx.set_state(
            indoc! {"
            Thˇe { quick } brown
            fox jumps over
            the {laˇzy} dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("c s { ]");
        cx.assert_state(
            indoc! {"
            The ˇ[quick] brown
            fox jumps over
            the ˇ[lazy] dog."},
            Mode::Normal,
        );

        // test multi cursor change with not exist surround
        cx.set_state(
            indoc! {"
            The {quˇick} brown
            fox jumps over
            the [laˇzy] dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("c s [ '");
        cx.assert_state(
            indoc! {"
            The {quick} brown
            fox jumps over
            the ˇ'lazy' dog."},
            Mode::Normal,
        );

        // test change nesting surrounds
        cx.set_state(
            indoc! {"
            fn test_surround() {
                ifˇ 2 > 1 {
                    ˇprintln!(\"it is fine\");
                }
            };"},
            Mode::Normal,
        );
        cx.simulate_keystrokes("c s } ]");
        cx.assert_state(
            indoc! {"
            fn test_surround() ˇ[
                if 2 > 1 ˇ[
                    println!(\"it is fine\");
                ]
            ];"},
            Mode::Normal,
        );

        // test spaces with quote change surrounds
        cx.set_state(
            indoc! {"
            fn test_surround() {
                \"ˇ \"
            };"},
            Mode::Normal,
        );
        cx.simulate_keystrokes("c s \" '");
        cx.assert_state(
            indoc! {"
            fn test_surround() {
                ˇ' '
            };"},
            Mode::Normal,
        );

        // Currently, the same test case but using the closing bracket `]`
        // actually removes a whitespace before the closing bracket, something
        // that might need to be fixed?
        cx.set_state(
            indoc! {"
            fn test_surround() {
                ifˇ 2 > 1 {
                    ˇprintln!(\"it is fine\");
                }
            };"},
            Mode::Normal,
        );
        cx.simulate_keystrokes("c s { ]");
        cx.assert_state(
            indoc! {"
            fn test_surround() ˇ[
                if 2 > 1 ˇ[
                    println!(\"it is fine\");
                ]
            ];"},
            Mode::Normal,
        );

        // test change quotes.
        cx.set_state(indoc! {"'  ˇstr  '"}, Mode::Normal);
        cx.simulate_keystrokes("c s ' \"");
        cx.assert_state(indoc! {"ˇ\"  str  \""}, Mode::Normal);

        // test multi cursor change quotes
        cx.set_state(
            indoc! {"
            '  ˇstr  '
            some example text here
            ˇ'  str  '
        "},
            Mode::Normal,
        );
        cx.simulate_keystrokes("c s ' \"");
        cx.assert_state(
            indoc! {"
            ˇ\"  str  \"
            some example text here
            ˇ\"  str  \"
        "},
            Mode::Normal,
        );

        // test quote to bracket spacing.
        cx.set_state(indoc! {"'ˇfoobar'"}, Mode::Normal);
        cx.simulate_keystrokes("c s ' {");
        cx.assert_state(indoc! {"ˇ{ foobar }"}, Mode::Normal);

        cx.set_state(indoc! {"'ˇfoobar'"}, Mode::Normal);
        cx.simulate_keystrokes("c s ' }");
        cx.assert_state(indoc! {"ˇ{foobar}"}, Mode::Normal);
    }

    #[gpui::test]
    async fn test_change_surrounds_any_brackets(cx: &mut gpui::TestAppContext) {
        let mut cx = VimTestContext::new(cx, true).await;

        // Update keybindings so that using `csb` triggers Vim's `AnyBrackets`
        // action.
        cx.update(|_, cx| {
            cx.bind_keys([KeyBinding::new(
                "b",
                AnyBrackets,
                Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
            )]);
        });

        cx.set_state(indoc! {"{braˇcketed}"}, Mode::Normal);
        cx.simulate_keystrokes("c s b [");
        cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal);

        cx.set_state(indoc! {"[braˇcketed]"}, Mode::Normal);
        cx.simulate_keystrokes("c s b {");
        cx.assert_state(indoc! {"ˇ{ bracketed }"}, Mode::Normal);

        cx.set_state(indoc! {"<braˇcketed>"}, Mode::Normal);
        cx.simulate_keystrokes("c s b [");
        cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal);

        cx.set_state(indoc! {"(braˇcketed)"}, Mode::Normal);
        cx.simulate_keystrokes("c s b [");
        cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal);

        cx.set_state(indoc! {"(< name: ˇ'Zed' >)"}, Mode::Normal);
        cx.simulate_keystrokes("c s b }");
        cx.assert_state(indoc! {"(ˇ{ name: 'Zed' })"}, Mode::Normal);

        cx.set_state(
            indoc! {"
            (< name: ˇ'Zed' >)
            (< nˇame: 'DeltaDB' >)
        "},
            Mode::Normal,
        );
        cx.simulate_keystrokes("c s b {");
        cx.set_state(
            indoc! {"
            (ˇ{ name: 'Zed' })
            (ˇ{ name: 'DeltaDB' })
        "},
            Mode::Normal,
        );
    }

    // The following test cases all follow tpope/vim-surround's behaviour
    // and are more focused on how whitespace is handled.
    #[gpui::test]
    async fn test_change_surrounds_vim(cx: &mut gpui::TestAppContext) {
        let mut cx = VimTestContext::new(cx, true).await;

        // Changing quote to quote should never change the surrounding
        // whitespace.
        cx.set_state(indoc! {"'  ˇa  '"}, Mode::Normal);
        cx.simulate_keystrokes("c s ' \"");
        cx.assert_state(indoc! {"ˇ\"  a  \""}, Mode::Normal);

        cx.set_state(indoc! {"\"  ˇa  \""}, Mode::Normal);
        cx.simulate_keystrokes("c s \" '");
        cx.assert_state(indoc! {"ˇ'  a  '"}, Mode::Normal);

        // Changing quote to bracket adds one more space when the opening
        // bracket is used, does not affect whitespace when the closing bracket
        // is used.
        cx.set_state(indoc! {"'  ˇa  '"}, Mode::Normal);
        cx.simulate_keystrokes("c s ' {");
        cx.assert_state(indoc! {"ˇ{   a   }"}, Mode::Normal);

        cx.set_state(indoc! {"'  ˇa  '"}, Mode::Normal);
        cx.simulate_keystrokes("c s ' }");
        cx.assert_state(indoc! {"ˇ{  a  }"}, Mode::Normal);

        // Changing bracket to quote should remove all space when the
        // opening bracket is used and preserve all space when the
        // closing one is used.
        cx.set_state(indoc! {"{  ˇa  }"}, Mode::Normal);
        cx.simulate_keystrokes("c s { '");
        cx.assert_state(indoc! {"ˇ'a'"}, Mode::Normal);

        cx.set_state(indoc! {"{  ˇa  }"}, Mode::Normal);
        cx.simulate_keystrokes("c s } '");
        cx.assert_state(indoc! {"ˇ'  a  '"}, Mode::Normal);

        // Changing bracket to bracket follows these rules:
        // * opening → opening – keeps only one space.
        // * opening → closing – removes all space.
        // * closing → opening – adds one space.
        // * closing → closing – does not change space.
        cx.set_state(indoc! {"{   ˇa   }"}, Mode::Normal);
        cx.simulate_keystrokes("c s { [");
        cx.assert_state(indoc! {"ˇ[ a ]"}, Mode::Normal);

        cx.set_state(indoc! {"{   ˇa   }"}, Mode::Normal);
        cx.simulate_keystrokes("c s { ]");
        cx.assert_state(indoc! {"ˇ[a]"}, Mode::Normal);

        cx.set_state(indoc! {"{  ˇa  }"}, Mode::Normal);
        cx.simulate_keystrokes("c s } [");
        cx.assert_state(indoc! {"ˇ[   a   ]"}, Mode::Normal);

        cx.set_state(indoc! {"{  ˇa  }"}, Mode::Normal);
        cx.simulate_keystrokes("c s } ]");
        cx.assert_state(indoc! {"ˇ[  a  ]"}, Mode::Normal);
    }

    #[gpui::test]
    async fn test_surrounds(cx: &mut gpui::TestAppContext) {
        let mut cx = VimTestContext::new(cx, true).await;

        cx.set_state(
            indoc! {"
            The quˇick brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("y s i w [");
        cx.assert_state(
            indoc! {"
            The ˇ[ quick ] brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );

        cx.simulate_keystrokes("c s [ }");
        cx.assert_state(
            indoc! {"
            The ˇ{quick} brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );

        cx.simulate_keystrokes("d s {");
        cx.assert_state(
            indoc! {"
            The ˇquick brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );

        cx.simulate_keystrokes("u");
        cx.assert_state(
            indoc! {"
            The ˇ{quick} brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );
    }

    #[gpui::test]
    async fn test_surround_aliases(cx: &mut gpui::TestAppContext) {
        let mut cx = VimTestContext::new(cx, true).await;

        // add aliases
        cx.set_state(
            indoc! {"
            The quˇick brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("y s i w b");
        cx.assert_state(
            indoc! {"
            The ˇ(quick) brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );

        cx.set_state(
            indoc! {"
            The quˇick brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("y s i w B");
        cx.assert_state(
            indoc! {"
            The ˇ{quick} brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );

        cx.set_state(
            indoc! {"
            The quˇick brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("y s i w a");
        cx.assert_state(
            indoc! {"
            The ˇ<quick> brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );

        cx.set_state(
            indoc! {"
            The quˇick brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("y s i w r");
        cx.assert_state(
            indoc! {"
            The ˇ[quick] brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );

        // change aliases
        cx.set_state(
            indoc! {"
            The {quˇick} brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("c s { b");
        cx.assert_state(
            indoc! {"
            The ˇ(quick) brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );

        cx.set_state(
            indoc! {"
            The (quˇick) brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("c s ( B");
        cx.assert_state(
            indoc! {"
            The ˇ{quick} brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );

        cx.set_state(
            indoc! {"
            The (quˇick) brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("c s ( a");
        cx.assert_state(
            indoc! {"
            The ˇ<quick> brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );

        cx.set_state(
            indoc! {"
            The <quˇick> brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("c s < b");
        cx.assert_state(
            indoc! {"
            The ˇ(quick) brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );

        cx.set_state(
            indoc! {"
            The (quˇick) brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("c s ( r");
        cx.assert_state(
            indoc! {"
            The ˇ[quick] brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );

        cx.set_state(
            indoc! {"
            The [quˇick] brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("c s [ b");
        cx.assert_state(
            indoc! {"
            The ˇ(quick) brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );

        // delete alias
        cx.set_state(
            indoc! {"
            The {quˇick} brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("d s B");
        cx.assert_state(
            indoc! {"
            The ˇquick brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );

        cx.set_state(
            indoc! {"
            The (quˇick) brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("d s b");
        cx.assert_state(
            indoc! {"
            The ˇquick brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );

        cx.set_state(
            indoc! {"
            The [quˇick] brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("d s r");
        cx.assert_state(
            indoc! {"
            The ˇquick brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );

        cx.set_state(
            indoc! {"
            The <quˇick> brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );
        cx.simulate_keystrokes("d s a");
        cx.assert_state(
            indoc! {"
            The ˇquick brown
            fox jumps over
            the lazy dog."},
            Mode::Normal,
        );
    }
}
